/*
 * Copyright (c) 2011-2012 Alexander Dubu
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * o  Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * o  Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * o  Neither the name Axil nor the names of its contributors may be used to
 *    endorse or promote products derived from this software without specific
 *    prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
package axil.api;

import axil.api.access.FieldDefinition;
import axil.api.error.AxilCompilationException;
import axil.api.error.AxilException;
import axil.api.extend.Environment;
import axil.api.extend.SourceAdapter;

import java.io.Reader;
import java.io.StringReader;
import java.io.Writer;
import java.lang.reflect.InvocationTargetException;


/**
 * The main entry point for the native Axil API.
 *
 * <h4></h4>
 * <h4>Compiling a Script</h4>
 * Compiling a script involves providing the Axil runtime with a context and the
 * source code to the script:
 *  <pre>
 *     Axil axil = Axil.instance();
 *     Context context = axil.context(
 *         axil.param("shoe-size"),
 *         axil.param("today"));
 *     Script script = axil.compile(axil.configuration(), context, source);
 * </pre>
 * Notice that the short form of the arg() method id used; Axil is not
 * interested in the value, only the fact that the name exists. Axil will not
 * attempt to traverse a host application object until runtime, but will make
 * a check that the host application objects referenced in the source will exist
 * at runtime.
 *
 * <h4>Running a Script</h4>
 * Executing a script involves compiling the script into an executable object
 * creating a context containing any host application objects accessible to the
 * script, and then executing the script in that context:
 * <pre>
 *     Axil axil = Axil.instance();
 *     Context context = axil.context(
 *         axil.param("shoe-size", 12),
 *         axil.param("today", new Date()));
 *     Script script = axil.compile(axil.configuration(), context, source);
 *     Environment env = axil.environment(axil.configuration(), context,
 *     		yourCurrentUser, null);
 *     AxilObject result = script.eval(env);
 *     Object value = result.asNative();
 * </pre>
 * Notice that the result of evaluating a script returns an object native to
 * Axil, not native to the host application. The host application's object is
 * returned via asNative() or one of the other methods that return a Java object.
 * As a convenience, the above form can also be expressed as:
 * <pre>
 *     Axil axil = Axil.instance();
 *     Object result = axil.eval(source, axil.arg("shoe-size", 12),
 *         axil.arg("today", new Date()));
 * </pre>
 *
 */
public abstract class Axil {
	/**
	 * This enumerated type controls the sensitivity of identifiers. This covers
	 * both values and function names. Note that the Axil standard library is
	 * coded to be case sensitive, so it functions correctly with either value.
	 */
	public enum Case {
		/**
		 * Identifiers encountered in the Axil script are case sensitive. That
		 * is, the name of the identifier as it was placed into a context must
		 * exactly match the name of the  identifier in the script. Case
		 * sensitivity is the default.
		 */
		SENSITIVE,

		/**
		 * Identifiers are case-insensitive. That is, case is ignored when
		 * looking up an identifier, so "shoe-size", "Shoe-Size" and "SHOE-SIZE"
		 * are all equivalent.
		 */
		INSENSITIVE
	}

	/**
	 * When performing field discovery on an accessible type, the Java-style
	 * fields and methods names are transformed into Axil identifiers. This
	 * enumerated type defines the rules that should be followed for the
	 * transformation.
	 */
	public enum Transformation {
		/**
		 * The first letter of the identifier is capitalized. For a method,
		 * the "is" or "get" prefix is removed. For example, the method
		 * getShoeSize() is transformed into the Axil identifier "ShoeSize".
		 * A field such as "shoeSize" is transformed into "ShoeSize".
		 */
		CAPITALIZE,

		/**
		 * The first letter of the identifier is made into lowercase. For a
		 * method like getShoeSize(), the Axil identifier will be "shoeSize".
		 * A field name is unchanged.
		 */
		CAMEL_CASE,

		/**
		 * The identifier is broken apart and has each element separated with
		 * dash characters. For example, the method getShoeSize() is transformed
		 * into the Axil identifier "shoe-size". A field such as "shoeSize" is
		 * transformed into "shoe-size". In keeping with the style of the Axil
		 * standard library, this is the default behavior.
		 */
		DASHES,

		/**
		 * The identifier is broken apart and transformed into a formal title.
		 * For example, a method such as getShoeSize() is transformed into the
		 * Axil identifier "Shoe Size". Likewise, a field such as "shoeSize" is
		 * transformed into "Shoe Size".
		 */
		TITLE
	}

	protected Axil() {
	}

	private static final Axil instance = load();
	private static Axil load() {
		try {
			return (Axil)Class.forName("axil.engine.AxilEngine").getMethod(
			           "instance").invoke(null);
		} catch (IllegalAccessException e) 	{
			throw new RuntimeException(e);
		} catch (InvocationTargetException e) {
			throw new RuntimeException(e);
		} catch (NoSuchMethodException e) 	{
			throw new RuntimeException(e);
		} catch (ClassNotFoundException e) 	{
			throw new RuntimeException(e);
		}
	}

	protected abstract AxilObject nil();


	/**
	 * Get a reference to the Axil runtime engine. This is the main entry point
	 * to the Axil native API.
	 *
	 * @return
	 * 	Returns a non-null instance of the runtime engine.
	 */
	public static Axil instance() {
		return instance;
	}


	/**
	 * A constant representing the Axil 'nil' object. The 'nil' object is Axil
	 * is not the same as null in Java -- it is a first-class, non-null object.
	 */
	public static final AxilObject NIL = instance.nil();


	/**
	 * Set the case sensitivity of the Axil language and how any transformations
	 * are performed on Java fields and method names. For example:
	 * <pre>
	 *     Axil.instance().identifiers(Case.INSENSITIVE, Transformation.TITLE);
	 * </pre>
	 * This example causes the language to treat identifiers the same as long
	 * as they contain the same characters regardless of case. Any Java fields
	 * accessible to an Axil script are transformed into human-readable titles.
	 * <p/>
	 * This setup operation must be performed before the first attempt to
	 * configure or execute an Axil script. Any attempt to perform this setup
	 * activity afterwards will result in an exception.
	 *
	 * @param sensitivity
	 * 	This enumerated type controls the sensitivity of identifiers. This
	 * 	covers both values and function names. Note that the Axil standard
	 * 	library is coded to be case sensitive, so it functions correctly with
	 * 	either value. Axil is case sensitive by default.
	 *
	 * @param transform
	 * 	When performing field discovery on an accessible type, the Java-style
	 * 	fields and methods names are transformed into Axil identifiers. This
	 * 	enumerated type defines the rules that should be followed for the
	 * 	transformation.
	 */
	public abstract void identifiers(Case sensitivity, Transformation transform);


	/**
	 * Get the case sensitivity for Axil identifiers. Axil is case sensitive
	 * by default.
	 *
	 * @return
	 * 	Returns the case sensitivity.
	 */
	public abstract Case sensitivity();


	/**
	 * Get the identifier transform currently in place. By default, Axil uses
	 * the DASHES transform, in keeping with the style of the Axil standard
	 * library.
	 *
	 * @return
	 * 	Returns the current transform.
	 */
	public abstract Transformation transform();


	/**
	 * Add a source adapter for locating source files. By default, there is only
	 * a single adapter installed, one for reading the Axil standard library
	 * from the Axil distribution. Additional adapters can be installed, forming
	 * a chain. The adapters are consulted in the reverse order of installation.
	 * That is, a custom adapter will be referenced before the built-in adapter.
	 * <p/>
	 * This setup operation must be performed before the first attempt to
	 * configure or execute an Axil script. Any attempt to perform this setup
	 * activity afterwards will result in an exception.
	 *
	 * @param adapter
	 * 	The adapter to be installed.
	 */
	public abstract void install(SourceAdapter adapter);


	/**
	 * Get the exact version and release information for the underlying runtime
	 * engine.
	 *
	 * @return
	 * 	Returns a string fully describing the runtime engine release information.
	 * 	The string returned is never null or empty.
	 */
	public abstract String release();


	/**
	 * Get the exact version of the Axil language supported by this API. The
	 * version information is in the form of: (major).(minor). For example,
	 * "1.0" or "1.1". The value returned is not the release information for
	 * the API, but the language version supported.
	 *
	 * @return
	 * 	Returns language version information. This string returned is a suitably
	 * 	stable format for an application to make compatibility decisions.
	 */
	public abstract String language();


	/**
	 * Manually set the accessor mechanisms for the given type. This mechanism
	 * is provided for manually setting up the accessibility when the annotation
	 * or interface-based mechanisms are not practical. Typically, this is only
	 * used when a type for which the source code is not available or cannot be
	 * altered needs to be accessible. This manual setup will completely override
	 * any interface or annotation discovery mechanism.
	 * <p/>
	 * All calls to this method must be performed before the first attempt is
	 * made to use a configuration or execute a script. Attempting this manual
	 * setup after Axil has been put into use will result in an exception.
	 *
	 * @param type
	 * 	The final, concrete class to be analyzed. An interface or abstract class
	 * 	cannot be specified. Any subclasses of the given class will not be
	 * 	recognized, only the exact given class.
	 *
	 * @param fields
	 * 	The set of fields to be made accessible. Only the fields explicitly
	 * 	specified will be allowed to be accessed.
	 *
	 * @see axil.api.access.AccessibleType
	 * @see axil.api.access.Accessible
	 */
	public abstract void access(Class type, FieldDefinition... fields);


	/**
	 * Get the default configuration. The configuration returned can be modified
	 * to alter the default, but will become immutable once put into use.
	 *
	 * @return
	 * 	Returns the default configuration. The default configuration will
	 * 	contain only the core module and the smallest possible memory and I/O
	 * 	settings as are practical to run modest-sized scripts.
	 */
	public abstract Configuration configuration();


	/**
	 * Return a formatting object based upon the given type and style
	 * specification string. The object returned may be a cached copy; this is
	 * harmless, since all Formatting objects are immutable and therefore
	 * thread-safe.
	 *
	 * @param type
	 * 	The Axil type to which the formatting object applies. The type given
	 * 	cannot be null.
	 *
	 * @param spec
	 * 	The specification for formatting. The specification format supported
	 * 	depends on the type. The string given cannot be null.
	 *
	 * @return
	 * 	Returns a formatting object, which is never null. Thew returned object
	 * 	can only be used with instances of the given type.
	 */
	public abstract Formatting formatting(AxilType type, String spec);


	/**
	 * Get an Axil locale from the given code. The code is composed of an ISO
	 * 639-1 language code (e.g. "en"), an underscore, and and ISO 3166-1
	 * country code (e.g. "US").
	 *
	 * @param code
	 * 	An Axil locale code, such as "en_US". The string given cannot be null
	 * 	and must be exactly five characters in length.
	 *
	 * @return
	 * 	A non-null locale is returned. An exception is thrown if the locale is
	 * 	not defined or if the locale is not enabled. See the Axil.cfg
	 * 	configuration file for details.
	 */
	public abstract Locale locale(String code);


	/**
	 * Get the synthetic persona associated with the Axil runtime environment.
	 * That persona is synthesized at start-up from Java environmental defaults.
	 *
	 * @return
	 * 	Returns the system persona, which is never null.
	 */
	public abstract Persona system();


	/**
	 * Get the Axil type metadata for the type with the given unique identity.
	 *
	 * @param ident
	 * 	The unique identity of the type.
	 *
	 * @return
	 * 	Returns a non-null AxilType object. An exception is thrown if the type
	 * 	is not one of the known types. This can be the result of a typo in the
	 * 	identity or a missing use() in the configuration so that the type has
	 * 	been excluded.
	 */
	public abstract AxilType type(String ident);


	/**
	 * Create an Argument object for the given name/value pair.
	 *
	 * @param key
	 * 	Any non-null string that contains no CR/LF characters nor a '.' (period)
	 * 	character. Axil allows spaces and punctuation in identifiers.
	 *
	 * @param value
	 * 	The value, which can be anything, including null.
	 *
	 * @return
	 * 	Returns the Argument object created.
	 */
	public abstract Argument arg(String key, Object value);


	/**
	 * Create an Argument object with a null value. This is a convenience
	 * method provided for the compilation of scripts. This cannot be used for
	 * values for script execution.
	 *
	 * @param name
	 * 	Any non-null string that contains no CR/LF characters nor a '.' (period)
	 * 	character. Axil allows spaces and punctuation in identifiers.
	 *
	 * @return
	 * 	Returns the Argument object created.
	 */
	public abstract Argument arg(String name);


	/**
	 * Create a new native, root context suitable for host application use. The
	 * context returned is mutable and has no parent.
	 *
	 * @param args
	 * 	A set of name/value pairs containing the host application objects
	 * 	available to the script.
	 *
	 * @return
	 * 	Returns a new context, which is never null.
	 */
	public Context context(Argument... args) {
		return context(null, args);
	}


	/**
	 * Create a new native context with the given object as the parent context.
	 *
	 * @param parent
	 * 	The parent context. The context given can be null, in which case a
	 * 	new root context is returned.
	 *
	 * @param args
	 * 	A set of name/value pairs containing the host application objects
	 * 	available to the script.
	 *
	 * @return
	 * 	Returns a new mutable context.
	 */
	public abstract Context context(Context parent, Argument... args);


	/**
	 * Create a new execution environment.
	 *
	 * @param config
	 * 	The configuration for this environment. The configuration will be sealed
	 * 	at this point. The configuration given cannot be null.
	 *
	 * @param context
	 * 	The context containing the host application objects. The context given
	 * 	cannot be null.
	 *
	 * @param persona
	 * 	The persona associated with tis environment. A null persona may be given
	 * 	in which case the system persona will be used.
	 *
	 * @param output
	 * 	The output device where any console output will be written. A null
	 * 	object may be used to indicate that all output should be discarded.
	 *
	 * @return
	 * 	Returns a new execution environment. The object returned is immutable
	 * 	and can be reused; but, the output device for script output may be
	 * 	written to, so do not reuse across thread boundaries unless the output
	 * 	device is thread safe.
	 *
	 * @see axil.api.Axil#system()
	 */
	public abstract Environment environment(Configuration config,
	                                        Context context, Persona persona, Writer output);


	/**
	 * Compile the script provided by the given reader. The reader given is
	 * automatically closed either when the end of the source is reached or if
	 * there are any I/O or compilation exceptions.
	 *
	 * @param config
	 * 	The configuration for this compilation. The configuration will be sealed
	 * 	at this point. The configuration given cannot be null.
	 *
	 * @param context
	 * 	The context under which compilation will be performed. The get() method
	 * 	on the context will not be invoked, but the accessor() method will be.
	 *
	 * @param name
	 * 	The name of this script. This name will be reporting as the source
	 * 	element in any errors occurring in relation to the script.
	 *
	 * @param source
	 * 	A reader that can provide the textual source code for the script.
	 *
	 * @return
	 * 	Returns a compiled script suitable for execution. A null value is
	 * 	never returned.
	 *
	 * @throws axil.api.error.AxilCompilationException
	 * 	This exception is thrown if there are any compilation errors in the
	 * 	production of the script.
	 */
	public abstract Script compile(Configuration config, Context context,
	                               String name, Reader source) throws AxilCompilationException;


	/**
	 * Compile the script given source, returning a script object.
	 *
	 * @param config
	 * 	The configuration for this compilation. The configuration will be sealed
	 * 	at this point. The configuration given cannot be null.
	 *
	 * @param context
	 * 	The context under which compilation will be performed. The get() method
	 * 	on the context will not be invoked, but the accessor() method will be.
	 *
	 * @param source
	 * 	The source code of the string.
	 *
	 * @return
	 * 	Returns a compiled script suitable for execution. A null value is
	 * 	never returned.
	 *
	 * @throws axil.api.error.AxilCompilationException
	 * 	This exception is thrown if there are any compilation errors in the
	 * 	production of the script.
	 */
	public Script compile(Configuration config, Context context, String source)
	throws AxilCompilationException {
		return compile(config, context, "script", new StringReader(source));
	}


	/**
	 * Compile and execute the given script, returning the native host
	 * application object resulting from the execution. The current default
	 * configuration is used; all console output from the script is discarded.
	 * This is strictly a convenience method.
	 *
	 * @param source
	 * 	The source code for the script.
	 *
	 * @param params
	 * 	A set of name/value pairs containing the host application objects
	 * 	available to the script.
	 *
	 * @return
	 * 	Returns the host application native object. The value is derived from
	 * 	invoking the asNative() method on the returned Axil object.
	 *
	 * @throws AxilException
	 * 	This exception is thrown if there is a compilation problem or a runtime
	 * 	problem.
	 */
	public Object eval(String source, Argument... params) throws AxilException {
		Context context = context(params);
		Configuration config = configuration();
		Script script = compile(config, context, source);
		Environment env = environment(config, context, system(), null);
		return script.eval(env).intrinsic();
	}
}
